/* * #%L * Nazgul Project: nazgul-core-xmlbinding-spi-jaxb * %% * Copyright (C) 2010 - 2017 jGuru Europe AB * %% * Licensed under the jGuru Europe AB license (the "License"), based * on Apache License, Version 2.0; you may not use this file except * in compliance with the License. * * You may obtain a copy of the License at * * http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% * */ package se.jguru.nazgul.core.xmlbinding.spi.jaxb.helper; import com.sun.xml.bind.marshaller.NamespacePrefixMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.ls.LSResourceResolver; import org.xml.sax.SAXException; import se.jguru.nazgul.core.algorithms.api.Validate; import se.jguru.nazgul.core.algorithms.api.collections.predicate.Tuple; import se.jguru.nazgul.core.algorithms.api.collections.predicate.common.ClassnameToClassTransformer; import se.jguru.nazgul.core.xmlbinding.spi.jaxb.ClassInformationHolder; import se.jguru.nazgul.core.xmlbinding.spi.jaxb.transport.EntityTransporter; import se.jguru.nazgul.core.xmlbinding.spi.jaxb.transport.JaxbConverterRegistry; import se.jguru.nazgul.core.xmlbinding.spi.jaxb.transport.type.JaxbAnnotatedNull; import javax.validation.constraints.NotNull; import javax.xml.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.SchemaOutputResolver; import javax.xml.bind.annotation.XmlTransient; import javax.xml.transform.Result; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * JAXB utility methods to simplify complex JAXB-related tasks. * * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB */ @SuppressWarnings("PMD.UnusedPrivateField") public abstract class JaxbUtils { // Our log private static final Logger log = LoggerFactory.getLogger(JaxbUtils.class.getName()); /** * The namespace prefix key for external (i.e. non-JDK-internal) JAXB distribution. */ private static final String EXTERNAL_JAXB_NAMESPACEPREFIXMAPPER_KEY = "com.sun.xml.bind.namespacePrefixMapper"; /** * The namespace prefix key for JDK-internal JAXB distribution. */ private static final String INTERNAL_JAXB_NAMESPACEPREFIXMAPPER_KEY = "com.sun.xml.internal.bind.namespacePrefixMapper"; /** * The java.lang.Class class. Don't add it to transport types. */ private static final String JAVALANGCLASS = "java.lang.Class"; private static final ClassnameToClassTransformer THREADLOCAL_TRANSFORMER = new ClassnameToClassTransformer(); // Internal state private static ConcurrentMap<SortedClassNameSetKey, JAXBContext> jaxbContextCache = new ConcurrentHashMap<SortedClassNameSetKey, JAXBContext>(); /** * Acquires a properly configured JAXB marshaller from the provided JAXBContext. * <pre><code> * // Acquire the JAXBContext for the types found within an EntityTransporter. * final NamespacePrefixMapper mapper = ... * final JAXBContext ctx = JaxbUtils.getJaxbContext(anEntityTransporter); * * // Now acquire the marshaller * final Marshaller marshaller = JaxbUtils.getHumanReadableStandardMarshaller(ctx, resolver); * * // ... and use it ... * marshaller.marshal(someInstance, someOutputTarget); * </code></pre> * * @param ctx The properly set up JAXBContext, holding all required class definitions. * @param namespacePrefixMapper A JAXB NamespacePrefixMapper to be used by the provided marshaller. * Cannot be {@code null}. * @param validate if {@code true}, performs validation - implying that the resourceResolver * must be non-null. * @return A standard JAXB Marshaller for human-readable (as opposed to extreme compactness) XML marshalling. * @throws NullPointerException if the {@code namespacePrefixMapper} was {@code null}. */ public static Marshaller getHumanReadableStandardMarshaller( @NotNull final JAXBContext ctx, @NotNull final NamespacePrefixMapper namespacePrefixMapper, final boolean validate) throws NullPointerException { // Check sanity Validate.notNull(namespacePrefixMapper, "namespacePrefixMapper"); try { // Acquire the properly configured Marshaller. final Marshaller toReturn = ctx.createMarshaller(); toReturn.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); toReturn.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); toReturn.setProperty(EXTERNAL_JAXB_NAMESPACEPREFIXMAPPER_KEY, namespacePrefixMapper); // Should we validate what we write? if (validate) { toReturn.setSchema(generateTransientXSD(ctx).getKey()); } return toReturn; } catch (final JAXBException e) { throw new IllegalStateException("Could not create marshaller", e); } } /** * Retrieves a JAXBContext instance, geared to converting all class types found within the * provided transporter. * * @param transporter The EntityTransporter which should be marshalled to XML. * @param isMarshalling {@code true} if the supplied JAXBContext is going to be used for marshalling, * as opposed to unmarshalling. * @return A JAXBContext able to marshal the provided transporter. * @throws NullPointerException if the transporter argument was {@code null}. */ @SuppressWarnings({"rawtypes", "unchecked"}) public static JAXBContext getJaxbContext(@NotNull final EntityTransporter transporter, final boolean isMarshalling) throws NullPointerException { // Check sanity Validate.notNull(transporter, "transporter"); // Does a cached JAXBContext exist? final Tuple<SortedClassNameSetKey, JAXBContext> cachedContext = getCachedJaxbContext(transporter.getClassInformation()); if (cachedContext.getValue() != null) { return cachedContext.getValue(); } // Load all relevant classes Set<Class<?>> loadedClasses = new HashSet<Class<?>>(); for (Object current : transporter.getClassInformation()) { final Class<?> classType = THREADLOCAL_TRANSFORMER.transform((String) current); addNonTransientInternalClass(loadedClasses, classType); } // If we are marshalling, acquire the non-transient internal // field types for the if (isMarshalling) { // The items List should be fully populated here. for (Object currentItem : transporter.getItems()) { addNonTransientInternalFieldTypes(loadedClasses, currentItem); } } try { // Create the JAXBContext and cache it. JAXBContext toReturn = JAXBContext.newInstance(loadedClasses.toArray(new Class[loadedClasses.size()])); jaxbContextCache.put(cachedContext.getKey(), toReturn); return toReturn; } catch (JAXBException e) { throw new IllegalStateException("Could not create JAXBContext", e); } } /** * Acquires a JAXB Schema from the provided JAXBContext. * * @param ctx The context for which am XSD should be constructed. * @return A tuple holding the constructed XSD from the provided JAXBContext, and * the LSResourceResolver synthesized during the way. * @throws NullPointerException if ctx was {@code null}. * @throws IllegalArgumentException if a JAXB-related exception occurred while extracting the schema. */ public static Tuple<Schema, LSResourceResolver> generateTransientXSD(@NotNull final JAXBContext ctx) throws NullPointerException, IllegalArgumentException { // Check sanity Validate.notNull(ctx, "ctx"); final SortedMap<String, ByteArrayOutputStream> namespace2SchemaMap = new TreeMap<String, ByteArrayOutputStream>(); try { ctx.generateSchema(new SchemaOutputResolver() { @Override public Result createOutput(final String namespaceUri, final String suggestedFileName) throws IOException { // The types should really be annotated with @XmlType(namespace = "... something ...") // to avoid using the default ("") namespace. if (namespaceUri.isEmpty()) { log.warn("Received empty namespaceUri while resolving a generated schema. " + "Did you forget to add a @XmlType(namespace = \"... something ...\") annotation " + "to your class?"); } // Create the result ByteArrayOutputStream final ByteArrayOutputStream out = new ByteArrayOutputStream(); final StreamResult toReturn = new StreamResult(out); toReturn.setSystemId(""); // Map the namespaceUri to the schemaResult. namespace2SchemaMap.put(namespaceUri, out); // All done. return toReturn; } }); } catch (IOException e) { throw new IllegalArgumentException("Could not acquire Schema snippets.", e); } // Convert to an array of StreamSource. final MappedSchemaResourceResolver resourceResolver = new MappedSchemaResourceResolver(); final StreamSource[] schemaSources = new StreamSource[namespace2SchemaMap.size()]; int counter = 0; for (Map.Entry<String, ByteArrayOutputStream> current : namespace2SchemaMap.entrySet()) { final byte[] schemaSnippetAsBytes = current.getValue().toByteArray(); resourceResolver.addNamespace2SchemaEntry(current.getKey(), new String(schemaSnippetAsBytes)); if (log.isDebugEnabled()) { log.info("Generated schema [" + (counter + 1) + "/" + schemaSources.length + "]:\n " + new String(schemaSnippetAsBytes)); } // Copy the schema source to the schemaSources array. schemaSources[counter] = new StreamSource(new ByteArrayInputStream(schemaSnippetAsBytes), ""); // Increase the counter counter++; } try { // All done. final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); schemaFactory.setResourceResolver(resourceResolver); final Schema transientSchema = schemaFactory.newSchema(schemaSources); // All done. return new Tuple<Schema, LSResourceResolver>(transientSchema, resourceResolver); } catch (final SAXException e) { throw new IllegalArgumentException("Could not create Schema from snippets.", e); } } /** * Extracts relevant data from the supplied {@code toWrapAndPackageForTransport} object, * appending the JAXB-transportable objects into the {@code resultingTransportableObjects} List, * and appending the transport type Class names into the {@code resultingTransportTypes} List. * * @param toWrapAndPackageForTransport The instance from which transport data should be extracted * and appended onto the resultingTransportableObjects and * resultingTransportTypes Lists, respectively. * @param registry The TransportTypeConverterRegistry instance used to extract * TransportTypeConverter instances, in turn used to translate * non-JAXB-annotated types to JaxbAnnotatedTypes for transport. * @param resultingTransportableObjects The non-null List holding JAXB-convertible objects. * @param resultingTransportTypes The non-null SortedSet holding the class names of all * transport types. */ public static void extractJaxbTransportData(final Object toWrapAndPackageForTransport, @NotNull final JaxbConverterRegistry registry, @NotNull final List<Object> resultingTransportableObjects, @NotNull final SortedSet<String> resultingTransportTypes) { // Check sanity Validate.notNull(registry, "registry"); Validate.notNull(resultingTransportableObjects, "resultingTransportableObjects"); Validate.notNull(resultingTransportTypes, "resultingTransportTypes"); // ### 1) Convert the current type if required. Object added = toWrapAndPackageForTransport; final Class<?> transportType = toWrapAndPackageForTransport == null ? JaxbAnnotatedNull.class : registry.getTransportType(toWrapAndPackageForTransport.getClass()); final boolean wrapInTransportType = added == null || added.getClass() != transportType; if (wrapInTransportType) { added = registry.packageForTransport(toWrapAndPackageForTransport); } // ### 2) Add the item itself. resultingTransportableObjects.add(added); // ### 3) Add the JAXB-annotated transport classes found within // the added object, via the use of its TypeExtractor methods. if (added instanceof ClassInformationHolder) { for (String current : ((ClassInformationHolder) added).getClassInformation()) { if (!resultingTransportTypes.contains(current) && !current.startsWith(JAVALANGCLASS)) { resultingTransportTypes.add(current); } } } // ### 4) Add the class of the added object itself. Handle Array types. final Class<?> addedClass = added.getClass(); final boolean addedClassIsArray = addedClass.isArray(); final String addedClassName = addedClassIsArray ? addedClass.getComponentType().getName() : addedClass.getName(); if (!addedClassName.startsWith(JAVALANGCLASS) && !resultingTransportTypes.contains(addedClassName) && !(addedClassIsArray && addedClass.getComponentType().isPrimitive())) { resultingTransportTypes.add(addedClassName); } // ### 5) Only add internal structure if the current class is not a AbstractJaxbAnnotatedTransportType. // Acquire internal reflected state as well. if (!wrapInTransportType) { final Set<Class<?>> internalReflectedTypes = new HashSet<Class<?>>(); final Set<Class<?>> transportTypes = new HashSet<Class<?>>(); addNonTransientInternalFieldTypes(internalReflectedTypes, toWrapAndPackageForTransport); for (Class<?> current : internalReflectedTypes) { // Don't add a null transport type. final Class<?> transportTypeOrNull = registry.getTransportType(current); final Class<?> toAdd = transportTypeOrNull == null ? current : transportTypeOrNull; addNonTransientInternalClass(transportTypes, toAdd); } // Fire the internally found reflected types through the JaxbConverter for (Class<?> current : transportTypes) { final String fullyQualifiedClassName = current.getName(); if (!resultingTransportTypes.contains(fullyQualifiedClassName)) { resultingTransportTypes.add(fullyQualifiedClassName); } } } } // // Private helpers // /** * Adds all field types found in the {@code toReflect} object to the provided types Set. * Should any Field in the {@code toReflect} instance be a Collection, Array or Map, any * classes found within the collections are added as well. * * @param types The resulting types set. * @param toReflect The object from whose internal Fields the type information should be extracted. */ @SuppressWarnings("rawtypes") private static void addNonTransientInternalFieldTypes(final Set<Class<?>> types, final Object toReflect) { // Check sanity if (toReflect == null) { return; } // First, add the current Class to the types Set final Class<?> theType = toReflect.getClass(); addNonTransientInternalClass(types, theType); // Don't reflect EntityTransporter classes if (theType == EntityTransporter.class) { return; } for (Field current : XmlMarshallableFieldPredicate.getMarshallableFields(toReflect)) { // Get the type of the object stored within the current field Object currentValue = get(current, toReflect); if (currentValue != null) { // Add the class of the currentValue final Class<?> currentType = currentValue.getClass(); addNonTransientInternalClass(types, currentType); // Handle Collections, Arrays and Maps, which may contain // implementation types which should be included. if (currentType.getClass().isArray()) { addNonTransientInternalClass(types, currentType.getComponentType()); } else if (Collection.class.isAssignableFrom(currentType)) { // This is a collection. Dig out the types of all Elements. for (Object currentElement : Collection.class.cast(get(current, toReflect))) { addNonTransientInternalFieldTypes(types, currentElement); } } else if (Map.class.isAssignableFrom(currentType)) { // This is a map. Dig out the types of all Keys and Value. final Map theMap = Map.class.cast(get(current, toReflect)); for (Object currentKey : theMap.keySet()) { addNonTransientInternalClass(types, currentKey.getClass()); final Object currentMapValue = theMap.get(currentKey); if (currentMapValue != null) { addNonTransientInternalClass(types, currentMapValue.getClass()); } } } } } } /** * Retrieves the value of the supplied Field from the given instance. * * @param aField The Field whose value should be retrieved. * @param anObject The instance whose class contains the supplied Field, and * from which the value should be retrieved. * @return the value of the supplied Field from the given instance. */ private static Object get(final Field aField, final Object anObject) { aField.setAccessible(true); try { return aField.get(anObject); } catch (Exception e) { return null; } } /** * Adds the given candidate Class to the supplied set of types, given that the candidate * passes some trivial checks. * * @param types The types set to which the candidate could be added. * @param candidate The type to [possibly] add to the given types Set. */ private static void addNonTransientInternalClass(final Set<Class<?>> types, final Class<?> candidate) { if (candidate == null) { return; } // Don't add XmlTransient classes final boolean isXmlTransient = candidate.getAnnotation(XmlTransient.class) != null; // Ignore interfaces final boolean isInterface = candidate.isInterface(); // Ignore primitives and Object final boolean isPrimitive = candidate.isPrimitive(); final boolean isObject = candidate == Object.class; // Ignore array types final boolean isArray = candidate.isArray(); if (!isXmlTransient && !isInterface && !isPrimitive && !isObject && !isArray) { types.add(candidate); } } /** * Creating new JAXBContexts is an expensive operation; caching a few pre-created ones and re-using * them speeds up the process of marshalling and unmarshalling considerably. * * @param classInformation The classes from an EntityTransporter for which a JAXBContext should be acquired. * @return A Tuple holding the best-match cache key for the EntityTransporter, and the corresponding * cached JAXBContext. The JAXBContext value might be {@code null}, indicating that a * cached JAXBContext was not found. */ private static Tuple<SortedClassNameSetKey, JAXBContext> getCachedJaxbContext( final SortedSet<String> classInformation) { // Create a sorted clone. final SortedSet<String> copy = new TreeSet<String>(classInformation); copy.add(EntityTransporter.class.getName()); final SortedClassNameSetKey key = new SortedClassNameSetKey(copy); JAXBContext jaxbContext = jaxbContextCache.get(key); if (jaxbContext != null) { // Exact existing match. All done. return new Tuple<SortedClassNameSetKey, JAXBContext>(key, jaxbContext); } // Search for matching/compatible SortedClassNameSetKeys. // The first matching one can be used for JAXBContext. Tuple<SortedClassNameSetKey, JAXBContext> toReturn = null; for (Map.Entry<SortedClassNameSetKey, JAXBContext> current : jaxbContextCache.entrySet()) { if (current.getKey().containsAll(copy)) { toReturn = new Tuple<SortedClassNameSetKey, JAXBContext>(current.getKey(), current.getValue()); } } if (toReturn == null) { // No cache entry found. Create one. toReturn = new Tuple<SortedClassNameSetKey, JAXBContext>(key, null); } // All done. return toReturn; } }